/*==========================================================================*\
 |  $Id: Interpreter.java 63 2010-01-21 21:52:50Z VTWoods $
 |*-------------------------------------------------------------------------*|
 |  Copyright (C) 2009 Virginia Tech
 |
 |  This file is part of CloudSpace.
 |
 |  CloudSpace is free software; you can redistribute it and/or modify
 |  it under the terms of the GNU General Public License as published
 |  by the Free Software Foundation; either version 3 of the License, or
 |  (at your option) any later version.
 |
 |  CloudSpace is distributed in the hope that it will be useful,
 |  but WITHOUT ANY WARRANTY; without even the implied warranty of
 |  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 |  GNU General Public License for more details.
 |
 |  You should have received a copy of the GNU General Public License
 |  along with CloudSpace; if not, see <http://www.gnu.org/licenses/>.
\*==========================================================================*/

package student.web.internal;

import java.io.File;
import java.io.FileFilter;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.zkoss.lang.Classes;
import org.zkoss.lang.reflect.Fields;
import org.zkoss.util.logging.Log;
import org.zkoss.xel.Function;
import org.zkoss.zk.scripting.Namespace;
import org.zkoss.zk.scripting.NamespaceChangeListener;
import org.zkoss.zk.scripting.SerializableAware;
import org.zkoss.zk.scripting.util.GenericInterpreter;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WebApp;

import bsh.BshClassManager;
import bsh.BshMethod;
import bsh.EvalError;
import bsh.NameSpace;
import bsh.Primitive;
import bsh.UtilEvalError;
import bsh.Variable;
import cloudspace.vm.VM;

/**
 * The interpreter that uses BeanShell to interpret zscript codes. Unlike the
 * default zk bean shell interpreter, this one is <b>not</b> hierarchically
 * aware.
 * 
 * @author tomyeh
 * @author Last changed by $Author: VTWoods $
 * @version $Revision: 63 $, $Date: 2009-10-20 14:33:41 -0400 (Tue, 20 Oct 2009)
 *          $
 */
public class Interpreter extends GenericInterpreter implements
		SerializableAware
{
	/**
	 * A variable of {@link Namespace}. The value is an instance of BeanShell's
	 * NameSpace.
	 */
	private static final String VAR_NS = "z_bshnS";
	private bsh.Interpreter _ip;
	private GlobalNS _bshns;
	Log log = Log.lookup(GenericInterpreter.class);

	// No limit on size of cache, or on age of loaders. Soft references
	// mean the garbage collector may take them at any time
	private static final MRUMap<String, ClassLoader> classLoaderCache = new MRUMap<String, ClassLoader>(
			0, 0);
	private static boolean setStorageBase = true;

	public Interpreter()
	{
		// nothing to do
	}

	// Deriving to override//
	/**
	 * Called when the top-level BeanShell namespace is created. By default, it
	 * does nothing.
	 * 
	 * <p>
	 * Note: to speed up the performance, this implementation disabled
	 * {@link bsh.NameSpace#loadDefaultImports}. It only imports the java.lang
	 * and java.util packages. If you want the built command and import
	 * packages, you can override this method. For example,
	 * 
	 * <pre>
	 * <code>
	 * rotected void loadDefaultImports(NameSpace bshns) {
	 *  bshns.importCommands("/bsh/commands");
	 * </code>
	 * </pre>
	 * 
	 * @param bshns
	 *            unused
	 * 
	 * @since 3.0.2
	 */
	protected void loadDefaultImports(NameSpace bshns)
	{
		// nothing to do
	}

	// GenericInterpreter//
	protected void exec(String script)
	{
		try
		{
			final Namespace ns = getCurrent();
			if (ns != null)
				_ip.eval(script, prepareNS(ns));
			else
				_ip.eval(script); // unlikely (but just in case)
		}
		catch (EvalError ex)
		{
			throw UiException.Aide.wrap(ex);
		}
	}

	protected boolean contains(String name)
	{
		try
		{
			return _ip.getNameSpace().getVariable(name) != Primitive.VOID;
			// Primitive.VOID means not defined
		}
		catch (UtilEvalError ex)
		{
			throw UiException.Aide.wrap(ex);
		}
	}

	protected Object get(String name)
	{
		try
		{
			return Primitive.unwrap(_ip.get(name));
		}
		catch (EvalError ex)
		{
			throw UiException.Aide.wrap(ex);
		}
	}

	protected void set(String name, Object val)
	{
		try
		{
			_ip.set(name, val);
			// unlike NameSpace.setVariable, _ip.set() handles null
		}
		catch (EvalError ex)
		{
			throw UiException.Aide.wrap(ex);
		}
	}

	protected void unset(String name)
	{
		try
		{
			_ip.unset(name);
		}
		catch (EvalError ex)
		{
			throw UiException.Aide.wrap(ex);
		}
	}

	// protected boolean contains(Namespace ns, String name) {
	// if (ns != null) {
	// final NameSpace bshns = prepareNS(ns);
	// //note: we have to create NameSpace (with prepareNS)
	// //to have the correct chain
	// if (bshns != _bshns) {
	// try {
	// return bshns.getVariable(name) != Primitive.VOID;
	// } catch (UtilEvalError ex) {
	// throw UiException.Aide.wrap(ex);
	// }
	// }
	// }
	// return contains(name);
	// }
	// protected Object get(Namespace ns, String name) {
	// if (ns != null) {
	// final NameSpace bshns = prepareNS(ns);
	// //note: we have to create NameSpace (with prepareNS)
	// //to have the correct chain
	// if (bshns != _bshns) {
	// try {
	// return Primitive.unwrap(bshns.getVariable(name));
	// } catch (UtilEvalError ex) {
	// throw UiException.Aide.wrap(ex);
	// }
	// }
	// }
	// return get(name);
	// }
	// protected void set(Namespace ns, String name, Object val) {
	// if (ns != null) {
	// final NameSpace bshns = prepareNS(ns);
	// //note: we have to create NameSpace (with prepareNS)
	// //to have the correct chain
	// if (bshns != _bshns) {
	// try {
	// bshns.setVariable(
	// name, val != null ? val: Primitive.NULL, false);
	// return;
	// } catch (UtilEvalError ex) {
	// throw UiException.Aide.wrap(ex);
	// }
	// }
	// }
	// set(name, val);
	// }
	// protected void unset(Namespace ns, String name) {
	// if (ns != null) {
	// final NameSpace bshns = prepareNS(ns);
	// //note: we have to create NameSpace (with prepareNS)
	// //to have the correct chain
	// if (bshns != _bshns) {
	// bshns.unsetVariable(name);
	// return;
	// }
	// }
	// unset(name);
	// }

	// CS 1705 Addition =================================================
	/** A file filter used to search for files to include on the classpath. */
	protected static final FileFilter JAR_FILTER = new FileFilter() {
		public boolean accept(File file)
		{
			String name = file.getName().toLowerCase();
			return name.endsWith(".jar") || name.endsWith(".zip");
		}
	};

	/** A file filter used to search for files to include on the classpath. */
	protected static final FileFilter CLASS_FILTER = new FileFilter() {
		public boolean accept(File file)
		{
			String name = file.getName().toLowerCase();
			return name.endsWith(".class");
		}
	};

	/** Add URLs for all jars in the given dir to the given list. */
	protected long addJarsToList(File dirToScan, List<URL> urls)
	{
		long result = 0;
		for (File jar : dirToScan.listFiles(JAR_FILTER))
		{
			result = Math.max(addDirToList(jar, urls), result);
		}
		return result;
	}

	/** Add a URL for the given file/dir to the given list. */
	protected long newestClassFileModTime(File file)
	{
		long result = 0L;
		if (file.isDirectory())
		{
			boolean deep = true;
			{
				File prune = new File(file, ".prune-classpath-search");
				deep = !prune.exists();
			}
			for (File f : file.listFiles())
			{
				if (CLASS_FILTER.accept(f))
				{
					result = Math.max(f.lastModified(), result);
				}
				else if (f.isDirectory() && deep)
				{
					result = Math.max(newestClassFileModTime(f), result);
				}
			}
		}
		return result;
	}

	/** Add a URL for the given file/dir to the given list. */
	protected long addDirToList(File dir, List<URL> urls)
	{
		try
		{
			urls.add(dir.toURL());
		}
		catch (MalformedURLException e)
		{
			log.warning("Error creating URL for " + dir, e);
		}

		return Math.max(dir.lastModified(), newestClassFileModTime(dir));
	}

	protected File dirForPage(Page page, WebApp app)
	{
		File pageFile = new File(app.getRealPath(page.getRequestPath()));
		File dir = pageFile.getParentFile();
		return dir;
	}

	/** Add the page's dir plus any jars in that dir to a list of URLs. */
	protected long extraClasspathFor(File dir, WebApp app, List<URL> list)
	{
		long result = Math.max(addDirToList(dir, list),
				addJarsToList(dir, list));
		// Add jars from the collection that are reloaded with every page:
		result = Math.max(addJarsToList(new File(app
				.getRealPath("/WEB-INF/pagelib")), list), result);
		return result;
	}

	// -- Interpreter --//
	public void init(Page owner, String zslang)
	{
		// System.out.println( getClass().getName() + ".init("
		// + owner.getRequestPath() + ", \"" + zslang + "\")" );
		super.init(owner, zslang);

		WebApp app = Executions.getCurrent().getDesktop().getWebApp();
		//TODO: Eventually this should be changed to use my VM stuff
		File pageDir = dirForPage(owner, app);

		String dirPath = pageDir.getAbsolutePath();
		_ip = new bsh.Interpreter();
		VM vm = VM.getThreadTag();
		vm.prepareInterpreter(_ip);
		try {
			_ip.eval("import cloudspace.util.jassistwrappers.*;");
		} catch (EvalError e) {
			System.err.println("Could not import javassist wrappers");
			e.printStackTrace();
		}
		if (setStorageBase)
		{
			PersistentStorageManager.setStorageLocation(new File(app
					.getRealPath("/WEB-INF/datastore")));
			setStorageBase = false;
		}
		/*
		 * ClassLoader target = null; List<URL> list = new ArrayList<URL>();
		 * long lastModTime = extraClasspathFor(pageDir, app, list);
		 * synchronized (classLoaderCache) { target =
		 * classLoaderCache.get(dirPath); long oldTime =
		 * classLoaderCache.keyLastSetTime(dirPath); if (target == null ||
		 * oldTime < lastModTime) { target = new URLClassLoader(
		 * list.toArray(new URL[list.size()]),
		 * Thread.currentThread().getContextClassLoader() );
		 * classLoaderCache.put(dirPath, target); Map<?,?> attrs =
		 * Sessions.getCurrent().getAttributes(); for (Object key :
		 * attrs.keySet()) { if (key.toString().contains("-")) {
		 * attrs.remove(key); } } } }
		 */
		_bshns = new GlobalNS(_ip.getClassManager(), "global");
		_ip.setNameSpace(_bshns);
		// attempt to force io helper cwd to the current page:
		try
		{
			interpret("student.IOHelper.setCurrentWorkingDirectory("
					+ "new java.io.File(\""
					+ pageDir.toString().replace('\\', '/') + "\"));", null);
		}
		catch (Exception e)
		{
			log.error(getClass().getName() + ".init(" + owner.getRequestPath()
					+ ", " + zslang + "): cannot set IOHelper cwd", e);
		}

	}

	// CS 1705 Addition =================================================

	public void destroy()
	{
		getOwner().getNamespace().unsetVariable(VAR_NS, false);

		// bug 1814819 ,clear variable, dennis
		try
		{
			_bshns.clear();
			_ip.setNameSpace(null);
		}
		catch (Throwable t)
		{ // silently ignore (in case of upgrading to new bsh)
		}

		_ip = null;
		_bshns = null;
		super.destroy();
	}

	/**
	 * Returns the native interpreter, or null if it is not initialized or
	 * destroyed. From application's standpoint, it never returns null, and the
	 * returned object must be an instance of {@link bsh.Interpreter}
	 * 
	 * @since 3.0.2
	 */
	public Object getNativeInterpreter()
	{
		return _ip;
	}

	public Class getClass(String clsnm)
	{
		try
		{
			return _bshns.getClass(clsnm);
		}
		catch (UtilEvalError ex)
		{
			throw new UiException("Failed to load class " + clsnm, ex);
		}
	}

	public Function getFunction(String name, Class[] argTypes)
	{
		return getFunction0(_bshns, name, argTypes);
	}

	// public Function getFunction(Namespace ns, String name, Class[] argTypes)
	// {
	// return getFunction0(prepareNS(ns), name, argTypes);
	// }
	private Function getFunction0(NameSpace bshns, String name, Class[] argTypes)
	{
		try
		{
			final BshMethod m = bshns.getMethod(name,
					argTypes != null ? argTypes : new Class[0], false);
			return m != null ? new BSHFunction(m) : null;
		}
		catch (UtilEvalError ex)
		{
			throw UiException.Aide.wrap(ex);
		}
	}

	/**
	 * Prepares the namespace for non-top-level namespace.
	 */
	private NameSpace prepareNS(Namespace ns)
	{
		if (ns == getOwner().getNamespace())
			return _bshns;

		NSX nsx = (NSX) ns.getVariable(VAR_NS, true);
		if (nsx != null)
			return nsx.ns;

		// bind bshns and ns
		Namespace p = ns.getParent();
		NameSpace bshns = // Bug 1831534: we have to pass class manager
		new NS(p != null ? prepareNS(p) : _bshns, _ip.getClassManager(), ns);
		// Bug 1899353: we have to use _bshns instead of null
		// Reason: unknown
		ns.setVariable(VAR_NS, new NSX(bshns), true);
		return bshns;
	}

	/** Prepares the namespace for detached components. */
	private static NameSpace prepareDetachedNS(Namespace ns)
	{
		NSX nsx = (NSX) ns.getVariable(VAR_NS, true);
		if (nsx != null)
			return nsx.ns;

		// bind bshns and ns
		Namespace p = ns.getParent();
		NameSpace bshns = new NS(p != null ? prepareDetachedNS(p) : null, null,
				ns);
		ns.setVariable(VAR_NS, new NSX(bshns), true);
		return bshns;
	}

	// supporting classes//
	/** The global namespace. */
	private static abstract class AbstractNS extends NameSpace
	{
		private boolean _inGet;

		protected AbstractNS(NameSpace parent, BshClassManager classManager,
				String name)
		{
			super(parent, classManager, name);
		}

		/** Deriver has to override this method. */
		abstract protected Object getFromNamespace(String name);

		// super//
		protected Variable getVariableImpl(String name, boolean recurse)
				throws UtilEvalError
		{
			// Note: getVariableImpl returns null if not defined,
			// while getVariable return Primitive.VOID if not defined

			// Tom M Yeh: 20060606:
			// We cannot override getVariable because BeanShell use
			// getVariableImpl to resolve a variable recusrivly
			//
			// setVariable will callback this method,
			// so use _inGet to prevent dead loop
			Variable var = super.getVariableImpl(name, recurse);
			if (!_inGet && var == null)
			{
				Object v = getFromNamespace(name);
				if (v != UNDEFINED)
				{
					// Variable has no public/protected contructor, so we have
					// to
					// store the value back (with setVariable) and retrieve
					// again
					_inGet = true;
					try
					{
						this.setVariable(name, v != null ? v : Primitive.NULL,
								false);
						var = super.getVariableImpl(name, false);
						this.unsetVariable(name); // restore
					}
					finally
					{
						_inGet = false;
					}
				}
			}
			return var;
		}

		public void loadDefaultImports()
		{
			// to speed up the formance
		}
	}

	/** The global NameSpace. */
	private class GlobalNS extends AbstractNS
	{
		private GlobalNS(BshClassManager classManager, String name)
		{
			super(null, classManager, name);
		}

		protected Object getFromNamespace(String name)
		{
			return Interpreter.this.getFromNamespace(name);
		}

		public void loadDefaultImports()
		{
			Interpreter.this.loadDefaultImports(this);
		}
	}

	/** The per-Namespace NameSpace. */
	private static class NS extends AbstractNS
	{
		private final Namespace _ns;

		private NS(NameSpace parent, BshClassManager classManager, Namespace ns)
		{
			super(parent, classManager, "ns" + System.identityHashCode(ns));
			_ns = ns;
			_ns.addChangeListener(new NSCListener(this));
		}

		// super//
		/** Search _ns instead. */
		protected Object getFromNamespace(String name)
		{
			final Interpreter ip = getInterpreter();
			return ip != null ? ip.getFromNamespace(_ns, name) : _ns
					.getVariable(name, false);
		}

		private Interpreter getInterpreter()
		{
			Page owner = _ns.getOwnerPage();
			if (owner != null)
			{
				for (Iterator it = owner.getLoadedInterpreters().iterator(); it
						.hasNext();)
				{
					final Object ip = it.next();
					if (ip instanceof Interpreter)
						return (Interpreter) ip;
				}
			}
			return null;
		}
	}

	private static class NSCListener implements NamespaceChangeListener
	{
		private final NS _bshns;

		private NSCListener(NS bshns)
		{
			_bshns = bshns;
		}

		public void onAdd(String name, Object value)
		{ /* unused */
		}

		public void onRemove(String name)
		{ /* unused */
		}

		public void onParentChanged(Namespace newparent)
		{
			if (newparent != null)
			{
				final Interpreter ip = _bshns.getInterpreter();
				_bshns.setParent(ip != null ? ip.prepareNS(newparent)
						: prepareDetachedNS(newparent));
				return;
			}

			_bshns.setParent(null);
		}
	}

	/**
	 * Non-serializable namespace. It is used to prevent itself from being
	 * serialized
	 */
	private static class NSX
	{
		NameSpace ns;

		private NSX(NameSpace ns)
		{
			this.ns = ns;
		}
	}

	// SerializableAware//
	public void write(java.io.ObjectOutputStream s, Filter filter)
			throws java.io.IOException
	{
		// 1. variables
		final String[] vars = _bshns.getVariableNames();
		for (int j = vars != null ? vars.length : 0; --j >= 0;)
		{
			final String nm = vars[j];
			if (nm != null && !"bsh".equals(nm))
			{
				final Object val = get(nm);
				if ((val == null || (val instanceof java.io.Serializable) || (val instanceof java.io.Externalizable))
						&& (filter == null || filter.accept(nm, val)))
				{
					s.writeObject(nm);
					s.writeObject(val);
				}
			}
		}
		s.writeObject(null); // denote end-of-vars

		// 2. methods
		final BshMethod[] mtds = _bshns.getMethods();
		for (int j = mtds != null ? mtds.length : 0; --j >= 0;)
		{
			final String nm = mtds[j].getName();
			if (filter == null || filter.accept(nm, mtds[j]))
			{
				// hack BeanShell 2.0b4 which cannot be serialized correctly
				Field f = null;
				boolean acs = false;
				try
				{
					f = Classes.getAnyField(BshMethod.class,
							"declaringNameSpace");
					acs = f.isAccessible();
					Fields.setAccessible(f, true);
					final Object old = f.get(mtds[j]);
					try
					{
						f.set(mtds[j], null);
						s.writeObject(mtds[j]);
					}
					finally
					{
						f.set(mtds[j], old);
					}
				}
				catch (java.io.IOException ex)
				{
					throw ex;
				}
				catch (Throwable ex)
				{
					throw UiException.Aide.wrap(ex);
				}
				finally
				{
					if (f != null)
						Fields.setAccessible(f, acs);
				}
			}
		}
		s.writeObject(null); // denote end-of-mtds

		// 3. imported class
		Field f = null;
		boolean acs = false;
		try
		{
			f = Classes.getAnyField(NameSpace.class, "importedClasses");
			acs = f.isAccessible();
			Fields.setAccessible(f, true);
			final Map clses = (Map) f.get(_bshns);
			if (clses != null)
				for (Iterator it = clses.values().iterator(); it.hasNext();)
				{
					final String clsnm = (String) it.next();
					if (!clsnm.startsWith("bsh."))
						s.writeObject(clsnm);
				}
		}
		catch (java.io.IOException ex)
		{
			throw ex;
		}
		catch (Throwable ex)
		{
			throw UiException.Aide.wrap(ex);
		}
		finally
		{
			if (f != null)
				Fields.setAccessible(f, acs);
		}
		s.writeObject(null); // denote end-of-cls

		// 4. imported package
		f = null;
		acs = false;
		try
		{
			f = Classes.getAnyField(NameSpace.class, "importedPackages");
			acs = f.isAccessible();
			Fields.setAccessible(f, true);
			final Collection pkgs = (Collection) f.get(_bshns);
			if (pkgs != null)
				for (Iterator it = pkgs.iterator(); it.hasNext();)
				{
					final String pkgnm = (String) it.next();
					if (!pkgnm.startsWith("java.awt")
							&& !pkgnm.startsWith("javax.swing"))
						s.writeObject(pkgnm);
				}
		}
		catch (java.io.IOException ex)
		{
			throw ex;
		}
		catch (Throwable ex)
		{
			throw UiException.Aide.wrap(ex);
		}
		finally
		{
			if (f != null)
				Fields.setAccessible(f, acs);
		}
		s.writeObject(null); // denote end-of-cls
	}

	public void read(java.io.ObjectInputStream s) throws java.io.IOException,
			ClassNotFoundException
	{
		for (;;)
		{
			final String nm = (String) s.readObject();
			if (nm == null)
				break; // no more

			set(nm, s.readObject());
		}

		try
		{
			for (;;)
			{
				final BshMethod mtd = (BshMethod) s.readObject();
				if (mtd == null)
					break; // no more

				// fix declaringNameSpace
				Field f = null;
				boolean acs = false;
				try
				{
					f = Classes.getAnyField(BshMethod.class,
							"declaringNameSpace");
					acs = f.isAccessible();
					Fields.setAccessible(f, true);
					f.set(mtd, _bshns);
				}
				catch (Throwable ex)
				{
					throw UiException.Aide.wrap(ex);
				}
				finally
				{
					if (f != null)
						Fields.setAccessible(f, acs);
				}

				_bshns.setMethod(mtd.getName(), mtd);
			}
		}
		catch (UtilEvalError ex)
		{
			throw UiException.Aide.wrap(ex);
		}

		for (;;)
		{
			final String nm = (String) s.readObject();
			if (nm == null)
				break; // no more

			_bshns.importClass(nm);
		}

		for (;;)
		{
			final String nm = (String) s.readObject();
			if (nm == null)
				break; // no more

			_bshns.importPackage(nm);
		}
	}

	private class BSHFunction implements Function
	{
		private final bsh.BshMethod _method;

		private BSHFunction(bsh.BshMethod method)
		{
			if (method == null)
				throw new IllegalArgumentException("null");
			_method = method;
		}

		// -- Function --//
		public Class[] getParameterTypes()
		{
			return _method.getParameterTypes();
		}

		public Class getReturnType()
		{
			return _method.getReturnType();
		}

		public Object invoke(Object obj, Object[] args) throws Exception
		{
			return _method.invoke(args != null ? args : new Object[0], _ip);
		}

		public java.lang.reflect.Method toMethod()
		{
			return null;
		}
	}
}
